22. 长三角零售销售预测(Walmart 方法对照)

本章概要

  • 学习材料:百联股份累计营收、披露版本和季度日期。
  • 本章任务:将累计值转单季,按时间留出比较同季朴素基线与趋势回归。
  • 完成后你将得到:真实值、两类预测、残差、MAPE、区间和采用决策表。
  • 自我检查:手算一个季度的 APE,并确认预测时没有使用未来数据;如果同一报告期有多个披露版本,应先确定采用哪一版。
  • 拓展练习:把预测方法的判断条件拓展应用到另一家长三角零售公司。

概念导览:预测流程四个抓手

  • 掌握零售业销售预测的完整流程
  • 理解特征工程与相关性分析的方法
  • 学会使用线性回归模型进行销售预测
  • 能够使用 \(R^2\) 评估模型效果

先凭直觉:上年同季就是第一条基线

季度 实际销售 上年同季预测 绝对百分比误差
2024Q1 110 100 \(|110-100|/110=9.1\%\)
2024Q2 96 90 \(|96-90|/96=6.3\%\)

先让模型回答“能否持续优于这条朴素规则”。MAPE 是各验证期绝对百分比误差的平均值;实际值为 0 或接近 0 时应换指标或单独披露。

时间轴:为什么不能随机切分?

2015–2022 训练 → 2023–2024 验证 → 2025 再次检查

  • 预测时只能使用当时已经披露的信息;随机切分会让未来季度进入训练,形成时间泄漏。
  • 训练期负责拟合缩放、特征与模型;验证期只用于比较基线、残差和 MAPE。
  • 即时判断:若回归 MAPE 不优于上年同季基线,拒绝候选模型,不因 \(R^2\) 较高而上线。

从直觉映射到完整实训

学生先掌握 完整代码中的变量 检查问题
上年同季基线 seasonal_baseline 是否使用相同验证窗?
时间边界 train / validation 验证期是否进入拟合?
误差 baseline_mape / regression_mape 候选是否真正优于基线?
拒绝规则 decision_status 何时保持基线或暂停?

理解这四项后,再学习怎样选择财报披露版本、把累计值换算为单季值、估计预测区间,以及发现数据问题时怎样检查原因。

中国主案例:百联股份季度营收预测

要素 课堂口径
本地资产 stock/financial_statement.h5(key: financial_data
对象与期间 上海百联股份 600827.SH,2015–2025 年季度营业收入
时间切分 2015–2022 训练,2023–2024 验证,2025 滚动再次检查
决策 预算与排班;验证期 MAPE ≤ 15% 才允许作为基线

Walmart 数据只用于完成平台练习。中国主案例先核实财报字段是累计值还是单季值,再与“上年同季”季节朴素基线比较,并报告披露版本、结构变化与预测区间。

财报口径与版本选择

风险 本章处理 检查依据
累计值误当单季值 Q1 取累计值,Q2–Q4 减去同年上期累计值 单季值与季度序号
同一报告期多版本 ann_date 时保留最晚披露;无版本字段且重复时停止实证 去重规则与重复计数

预测与验证风险

风险 本章处理 检查依据
季节性被趋势掩盖 朴素基线使用上年同季,回归加入季度虚拟变量 同一验证窗口的 MAPE
点预测被误当保证 用训练残差构造课堂近似 95% 区间 上下界及覆盖说明

本地预测示例:百联股份与朴素基线

Listing 1: 季度营业收入的时间切分与基线误差
展开本地预测示例完整代码
from pathlib import Path  # 导入Path以选择跨平台数据根目录
import pandas as pd  # 导入Pandas用于财报时间序列处理
import numpy as np  # 导入NumPy用于有限值检查要求
from sklearn.linear_model import LinearRegression  # 导入可解释线性回归作为候选模型
from sklearn.metrics import r2_score  # 导入R方用于描述验证期拟合边界

linux_root = Path('/home/ubuntu/r2_data_mount/data')  # 指定Linux课程数据根目录
windows_root = Path(r'C:\qiufei\data')  # 指定Windows课程数据根目录
data_root = linux_root if linux_root.exists() else windows_root  # 选择可用数据根目录
financial_path = data_root / 'stock' / 'financial_statement.h5'  # 定位财务报表本地快照

is_real_data_available = financial_path.exists()  # 将数据状态与渲染成功明确分离
if not is_real_data_available:  # 数据缺失时立即报告暂停但保持课件可渲染
    print(f'未找到课程数据文件:{financial_path};请从课程数据下载入口获取后重新运行。')  # 提示读者下载所需文件

完整实训 1/3:披露版本要求

Listing 2
展开披露版本约定
if is_real_data_available:  # 只在真实本地快照存在时定义披露选择约定
    financials = pd.read_hdf(financial_path, key='financial_data', where="order_book_id == '600827.XSHG'", columns=['order_book_id','quarter','info_date','operating_revenue']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','info_date':'ann_date','operating_revenue':'total_revenue'})  # 只读取目标公司的必需财报字段并标准化
    financials['ts_code'] = financials['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 统一证券代码后缀
    financials['end_date'] = pd.PeriodIndex(financials['end_date'].str.upper(), freq='Q').end_time.normalize()  # 将季度键转换为报告期末日期
    def select_latest_disclosure(frame,value_fields):  # 统一财报披露版本选择约定
        required={'ts_code','end_date',*value_fields}; versions=[c for c in ['ann_date','f_ann_date'] if c in frame.columns]  # 固定键与可接受版本字段
        if not required<=set(frame.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺列时停止
        work=frame.dropna(subset=list(required)).copy(); work['end_date']=pd.to_datetime(work['end_date'],errors='coerce')  # 清理必需字段
        for column in versions: work[column]=pd.to_datetime(work[column],errors='coerce')  # 解析披露日期
        keys=['ts_code','end_date']; work=work.dropna(subset=['end_date']); duplicate_mask=work.duplicated(keys,keep=False); before=int(duplicate_mask.sum())  # 统计选择前重复
        if before and (not versions or work.loc[duplicate_mask,versions].isna().all(axis=1).any()): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 无版本依据时停止
        work['_version']=work[versions].max(axis=1) if versions else pd.NaT  # 合并双版本字段为排序键
        latest_version=work.groupby(keys)['_version'].transform('max') if versions else pd.Series(pd.NaT,index=work.index)  # 为每个公司报告期定位最晚披露时点
        latest_rows=work.loc[~duplicate_mask | work['_version'].eq(latest_version)].copy()  # 仅保留单例或并列最晚版本
        tied_mask=latest_rows.duplicated(keys,keep=False); tied_latest_rows=int(tied_mask.sum())  # 统计并列最晚版本行
        conflicting_latest_keys=int(latest_rows.loc[tied_mask].groupby(keys,dropna=False)[value_fields].nunique(dropna=False).gt(1).any(axis=1).sum()) if tied_latest_rows else 0  # 统计并列且数值冲突的披露键
        if conflicting_latest_keys: raise ValueError(f'输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 并列最新值冲突时请先检查数据后再继续
        selected=latest_rows.drop_duplicates(keys+value_fields).drop_duplicates(keys).drop(columns='_version',errors='ignore')  # 仅合并数值完全一致的并列行
        after=int(selected.duplicated(keys).sum()); audit={'duplicate_rows_before':before,'tied_latest_rows':tied_latest_rows,'conflicting_latest_keys':conflicting_latest_keys,'duplicate_rows_after':after,'version_fields':versions}  # 返回并列与冲突检查
        if after: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 选择后仍重复则停止
        return selected,audit  # 返回唯一版本与检查依据

完整实训 2/3:累计收入转单季

Listing 3
展开单季收入转换
if is_real_data_available:  # 数据可用时执行披露选择与累计转单季
    bailian,version_audit = select_latest_disclosure(financials.loc[financials['ts_code'].eq('600827.SH')],['total_revenue'])  # 筛选百联并保留最晚披露版本
    print('披露数据版本检查:',version_audit)  # 报告选择前后重复与采用字段
    bailian = bailian.loc[bailian['end_date'].between('2015-01-01', '2025-12-31')].sort_values('end_date').copy()  # 固定课堂样本边界与时间顺序
    bailian['fiscal_year'] = bailian['end_date'].dt.year  # 构造财政年键
    bailian['quarter'] = bailian['end_date'].dt.quarter  # 构造财政季度键
    if bailian.duplicated(['fiscal_year','quarter']).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 公司期间键不唯一时停止
    prior_cumulative=bailian[['fiscal_year','quarter','total_revenue']].rename(columns={'total_revenue':'prior_cumulative_revenue'}).copy()  # 建立前一财政季度查找表
    prior_cumulative['quarter']=prior_cumulative['quarter']+1  # 将每期键前移到其紧邻后季
    bailian=bailian.merge(prior_cumulative,on=['fiscal_year','quarter'],how='left',validate='one_to_one')  # 按同年紧邻季度精确连接累计值
    missing_predecessor=bailian['quarter'].gt(1) & bailian['prior_cumulative_revenue'].isna()  # 识别Q2至Q4缺失紧邻前季的情形
    if missing_predecessor.any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 累计转单季无法精确相减时停止
    bailian['quarter_revenue'] = np.where(bailian['quarter'].eq(1),bailian['total_revenue'],bailian['total_revenue']-bailian['prior_cumulative_revenue'])  # 用Q1累计值或紧邻累计差得到单季收入
披露数据版本检查: {'duplicate_rows_before': 0, 'tied_latest_rows': 0, 'conflicting_latest_keys': 0, 'duplicate_rows_after': 0, 'version_fields': ['ann_date']}

完整实训 3/3:上年同季基线

Listing 4
展开上年同季基线构造
if is_real_data_available:  # 数据可用时构造上年同季基线与时间特征
    prior_season=bailian[['fiscal_year','quarter','quarter_revenue']].rename(columns={'quarter_revenue':'seasonal_baseline'}).copy()  # 建立上年同季查找表
    prior_season['fiscal_year']=prior_season['fiscal_year']+1  # 将历史年键前移一年以匹配当期
    bailian=bailian.merge(prior_season,on=['fiscal_year','quarter'],how='left',validate='one_to_one')  # 按相同财政季度精确连接上年基线
    bailian['trend_index'] = range(len(bailian))  # 构造只使用时间顺序的趋势特征

同一时间段模型比较与近似预测区间

Listing 5: 季节基线、趋势回归与区间评估
展开模型比较与区间代码
if is_real_data_available:  # 仅对已成功读取的真实单季数据评估
    model_frame = bailian.dropna(subset=['quarter_revenue', 'seasonal_baseline']).copy()  # 排除缺少同季基线的记录
    model_features = pd.get_dummies(model_frame[['trend_index', 'quarter']], columns=['quarter'], drop_first=True)  # 结合趋势与季节虚拟变量
    training_mask = model_frame['end_date'].lt('2023-01-01')  # 禁止验证期信息进入训练
    validation_mask = model_frame['end_date'].between('2023-01-01', '2024-12-31')  # 固定同一验证窗口
    if training_mask.sum()<8 or validation_mask.sum()<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 拟合前检查窗口
    if model_frame.loc[validation_mask,'quarter_revenue'].eq(0).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 零实际值禁止进入MAPE
    regression = LinearRegression().fit(model_features.loc[training_mask], model_frame.loc[training_mask, 'quarter_revenue'])  # 拟合可解释候选模型
    model_frame.loc[validation_mask, 'regression_forecast'] = regression.predict(model_features.loc[validation_mask])  # 生成验证期预测
    training_residuals = model_frame.loc[training_mask, 'quarter_revenue'] - regression.predict(model_features.loc[training_mask])  # 估计训练残差波动
    interval_half_width = 1.96 * training_residuals.std(ddof=1)  # 仅在独立同分布、近似正态同方差且忽略参数不确定性时作课堂95%近似
    validation = model_frame.loc[validation_mask].copy()  # 隔离用于决策的验证样本
    validation['prediction_lower'] = validation['regression_forecast'] - interval_half_width  # 计算近似区间下界
    validation['prediction_upper'] = validation['regression_forecast'] + interval_half_width  # 计算近似区间上界
    baseline_mape = ((validation['quarter_revenue'] - validation['seasonal_baseline']).abs() / validation['quarter_revenue'].abs()).mean()  # 计算季节朴素基线MAPE
    regression_mape = ((validation['quarter_revenue'] - validation['regression_forecast']).abs() / validation['quarter_revenue'].abs()).mean()  # 计算候选模型MAPE
    validation_r2 = r2_score(validation['quarter_revenue'], validation['regression_forecast'])  # 报告拟合边界而非决策收益
    if not np.isfinite([baseline_mape,regression_mape,validation_r2,interval_half_width]).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 非有限依据不得进入决策
    validation_display=validation[['end_date','quarter_revenue','seasonal_baseline','regression_forecast','prediction_lower','prediction_upper']].copy()  # 建立紧凑课堂展示表
    validation_display['end_date']=validation['end_date'].dt.year.astype(str)+'-Q'+validation['quarter'].astype(str)  # 用短季度标签替代长时间戳
    validation_display.columns=['季度','实际(亿元)','季节基线','趋势模型','区间下界','区间上界']  # 使用短列名避免输出换行
    validation_display.iloc[:,1:]=validation_display.iloc[:,1:].div(1e8).round(1)  # 统一为亿元并控制一位小数
    print(validation_display.to_string(index=False))  # 每个验证季度仅占一行
    print({'base_mape':round(baseline_mape,3),'model_mape':round(regression_mape,3),'R2':round(validation_r2,3),'95%半宽_亿元':round(interval_half_width/1e8,1)})  # 用简短键单行输出同一时间段指标
     季度  实际(亿元)  季节基线  趋势模型  区间下界  区间上界
2023-Q1    94.9 102.9  92.7  70.5 114.8
2023-Q2    68.5  68.2  68.5  46.4  90.7
2023-Q3    72.3  80.0  66.9  44.7  89.0
2023-Q4    69.4  71.8  75.7  53.6  97.9
2024-Q1    88.3  94.9  85.3  63.1 107.4
2024-Q2    63.4  68.5  61.2  39.0  83.3
2024-Q3    64.4  72.3  59.5  37.3  81.6
2024-Q4    60.6  69.4  68.4  46.2  90.5
{'base_mape': 0.081, 'model_mape': 0.058, 'R2': 0.836, '95%半宽_亿元': 22.1}

区间边界与阈值解释

区间边界:这是基于训练残差的教学近似,并非经过异方差、参数不确定性和结构突变校正的正式预测区间。

阈值来源:MAPE 15% 是课堂预算情景假设。决策题必须比较 10%、15%、20% 三档阈值,并说明误差成本如何改变是否采用模型。

预测结果与说明

  • 预期表:每个验证季度包含单季真实收入、上年同季基线、回归预测及近似 95% 区间;另报两种 MAPE 与回归 \(R^2\)
  • 采用规则:候选模型必须优于朴素基线且通过课堂成本阈值;否则保留基线并调查季节性、结构突变与数据修订。

参考答案状态:不用构造数字代替实证

  • 完成数据下载后:运行以上两个代码块,把实际结果与所用字段、披露版本和样本时间范围一起写出。
  • 公开教材读者:先从课程数据下载入口下载财务报表文件,再按本章说明设置文件位置。
  • 读取后检查:确认累计值差分、重复披露处理和验证时间段均符合本章要求。

销售预测的商业价值

销售预测是零售企业运营管理的核心能力:

  • 库存管理:避免缺货或积压,降低仓储成本
  • 人员安排:高峰期增加人手,提升服务质量
  • 营销策略:选择最佳促销时机,最大化收益
  • 财务规划:精准预测现金流,优化资金使用

项目分析流程概览

销售预测分析流程 从数据准备到模型评估的五步流程图 数据读取 读取 CSV 特征工程 日期·编码 相关性分析 特征筛选 模型训练 线性回归 模型评估 MAPE 主案例:单季收入 → 时间留出 → 基线对比 → 阈值决策

数据集介绍

本案例使用 沃尔玛超市销售数据01_Walmart_Store_sales.csv):

字段名 含义 类型
Store 门店编号 分类
Date 日期 日期
Weekly_Sales 周销售额(目标变量) 数值
Holiday_Flag 是否节假日 二值
Temperature 当地温度 数值

数据集字段:外部环境

字段名 含义 类型
Fuel_Price 当地燃油价格 数值
CPI 消费价格指数 数值
Unemployment 失业率 数值

解读边界:外部变量可用于比较和建模,不能只凭相关系数解释因果。

运行前预测|平台任务:完整预测代码

  • 输入预测:运行前先写出 dftargety_varsdf1 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到“R平方值(R^2):”相关结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务:完整预测代码”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务:完整预测代码

展开完整代码(投影默认折叠)
# 注:01_Walmart_Store_sales.csv数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 导入相关包
import pandas as pd
import numpy as np  # 导入NumPy数值计算库
import seaborn as sns  # 导入Seaborn可视化库
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
from sklearn.preprocessing import StandardScaler  # 导入Scikit-learn的StandardScaler模块
from sklearn.model_selection import train_test_split  # 导入Scikit-learn的train_test_split模块
from sklearn.linear_model import LinearRegression  # 导入Scikit-learn的LinearRegression模块
from sklearn.metrics import r2_score  # 导入Scikit-learn的r2_score模块
 
# 读取数据
df = pd.read_csv('01_Walmart_Store_sales.csv', parse_dates = True, infer_datetime_format = True)
 
# 整理数据
df.Date=pd.to_datetime(df.Date)
 
df['weekday'] = df.Date.dt.weekday  # 提取日期时间属性
df['month'] = df.Date.dt.month  # 提取日期时间属性
df['year'] = df.Date.dt.year  # 提取日期时间属性
# 删除日期、星期、年份等非特征列
df.drop(['Date','weekday','year'], axis=1, inplace=True)
target = 'Weekly_Sales'  # 指定目标变量为"Weekly_Sales"
 
# 相关性分析和特征选择
plt.figure(figsize=(10,8))
# 绘制变量两两关系图
sns.pairplot(df, x_vars=['Holiday_Flag', 'Temperature', 'Fuel_Price', 'CPI', 'Unemployment','month'],
            y_vars=['Weekly_Sales'], height=4, aspect=0.7, kind='reg')  # 定义列表y_vars
plt.savefig("1.png")  # 保存图形至文件
plt.show()  # 显示图形
 
df1=df.copy(deep=True)  # 深拷贝数据框用于独立分析(不影响原始数据)
df1.drop(['Store'], axis=1, inplace=True)  # 删除不需要的列'Store'
corr_matrix = df1.corr()  # 计算相关系数矩阵
plt.figure(figsize=(10,8))  # 创建图形画布
sns.heatmap(corr_matrix, annot=True, cmap='Reds')  # 绘制热力图
plt.savefig("2.png")  # 保存图形至文件
plt.show()  # 显示图形
 
df.drop(['Fuel_Price'], axis=1, inplace=True)  # 删除不需要的列'Fuel_Price'
# 分类数据处理
df = pd.get_dummies(df, columns=['Store','month'])
 
# 提取特征和标签
X = df.drop([target],axis=1)
y = df[target]  # 提取目标变量列赋值给y
 
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, test_size=0.2, random_state=100)

# 数据标准化
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)  # 对数据进行变换
X_test = scaler.transform(X_test)  # 对数据进行变换
 
# 训练多元线性回归模型
model = LinearRegression()
model.fit(X_train, y_train)  # 在数据上训练model模型
 
# 测试集预测
y_pred = model.predict(X_test)
r2 =  r2_score(y_test, y_pred)  # 计算R²评估模型拟合优度
# 评估模型预测效果
print('R平方值(R^2):', r2)
 

任务复盘|平台任务:完整预测代码

依据诊断:该固定答案的低边际相关删特征、随机切分、原始系数大小与单一 R² 不能支持预测部署或业务影响结论。正确的分析方法必须按时间留出,只在训练折内拟合预处理,用特征消融/滚动验证决定删留,在同一验证窗比较季节朴素基线、成本误差,并检查残差与漂移。

运行后核对:平台内只核对固定录入与输出结构;不得把该结果计入完成本章。参考答案必须写出前述正确的分析方法及明确的需要重新检查的情况。

拓展练习:把对象或期间改为一家长三角上市公司或一组 A 股资产;先预测指标方向,再说明结果能支持和不能支持的决策。

第一步:导入库与读取数据

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
  • 数据处理:pandasnumpy
  • 可视化:seabornmatplotlib
  • 机器学习:sklearn(标准化、划分、回归、评估)

第二步:读取数据与日期解析

# 读取CSV数据
df = pd.read_csv('01_Walmart_Store_sales.csv',
                 parse_dates=True,
                 infer_datetime_format=True)

# 将Date列转换为日期类型
df.Date = pd.to_datetime(df.Date)
  • parse_dates=True:自动尝试解析日期列
  • pd.to_datetime():确保日期列格式正确

第三步:提取日期特征

# 从日期中提取时间特征
df['weekday'] = df.Date.dt.weekday   # 星期几(0=周一)
df['month'] = df.Date.dt.month       # 月份(1-12)
df['year'] = df.Date.dt.year         # 年份

# 删除原始日期列及冗余特征
df.drop(['Date', 'weekday', 'year'],
        axis=1, inplace=True)

保留月份的原因:销售额具有明显的季节性规律

特征选择的直觉理解

特征候选的验证逻辑 保留全部候选特征,通过训练期消融和时间验证决定是否保留 全部候选特征 Holiday_Flag Temperature Fuel_Price CPI Unemployment month Store 训练期消融 时间验证后决策 数值特征 Holiday_Flag, Temperature Fuel_Price, CPI Unemployment 分类特征 Store, month → 独热编码 特征矩阵 X 输入模型训练

第四步:绘制散点图矩阵

展开完整代码(投影默认折叠)
# 绘制各特征与目标变量的关系
sns.pairplot(
    df,
    x_vars=['Holiday_Flag', 'Temperature',
            'Fuel_Price', 'CPI',
            'Unemployment', 'month'],
    y_vars=['Weekly_Sales'],
    height=4, aspect=0.7, kind='reg'
)
plt.show()
  • kind='reg':添加回归拟合线,直观判断线性关系强弱
  • 斜率受特征单位、门店差异、季节与异常值影响,只描述当前边际关联;不能据此比较重要性或宣称业务影响

第五步:绘制相关性热力图

# 深拷贝数据,删除分类列
df1 = df.copy(deep=True)
df1.drop(['Store'], axis=1, inplace=True)

# 计算皮尔逊相关系数矩阵
corr_matrix = df1.corr()

# 绘制热力图
plt.figure(figsize=(10, 8))
sns.heatmap(corr_matrix, annot=True, cmap='Reds')
plt.show()
  • annot=True:在格子中显示相关系数值
  • 颜色越深 → 相关性越强

如何解读相关性热力图

相关系数范围 描述 合法下一步
0.7 ~ 1.0 强正线性关联 检查共线性、时间稳定性与泄漏
0.3 ~ 0.7 中等正线性关联 纳入训练期消融或滚动验证候选
0 ~ 0.3 弱边际线性关联 仍保留候选;检查非线性、交互与增量价值
负值 负线性关联 解释方向前先核对尺度、分组与时间结构

诊断|低相关不等于无预测价值

诊断结论Fuel_Price 的低边际相关不能推出“无预测价值”,也不能证明因果。是否删除只能由训练期内特征消融/滚动验证决定,并在同一时间留出集上比较季节朴素基线、成本误差与残差稳定性。

  • 保留候选:先检查非线性、交互、分组与时间稳定性。
  • 删除条件:训练期内消融后,同一时间段样本外表现不降且复杂度/成本更低。
  • 遇到问题时:没有时间留出与朴素基线时,不发布特征删留结论。

第六步:分类变量独热编码

# 保留低边际相关候选,交由训练期内消融验证决定是否删除
candidate_features = df.copy(deep=True)

# 对分类变量进行独热编码
df = pd.get_dummies(candidate_features, columns=['Store', 'month'])

独热编码 将分类变量转换为 0/1 指示变量:

  • Store 列(45个门店)→ 生成 45 个新列
  • month 列(12个月份)→ 生成 12 个新列
  • 每行只有一个对应列为 1,其余为 0

第七步:划分训练集与测试集

平台练习说明:下方随机 train_test_split 只为受保护平台步骤兼容,不可作为时间序列上线依据。它会打乱信息边界,也没有季节朴素基线;中国主案例必须按时间留出。

方法要求:随机划分会打乱时间顺序,不能作为本章的主要预测方法。应按时间先后划分训练期和验证期,并与同一时期的季节基准预测比较,同时检查残差和数据分布变化。

# 提取特征和目标变量
X = df.drop([target], axis=1)
y = df[target]

# 80%训练,20%测试
X_train, X_test, y_train, y_test = \
    train_test_split(X, y,
                     train_size=0.8,
                     test_size=0.2,
                     random_state=100)
  • random_state=100:固定随机种子,保证每次运行都能得到一致结果
  • 训练集用于学习规律,测试集用于验证效果

第八步:数据标准化

# 创建标准化器
scaler = StandardScaler()

# 在训练集上拟合并转换
X_train = scaler.fit_transform(X_train)

# 用训练集参数转换测试集
X_test = scaler.transform(X_test)

标准化公式\(z = \frac{x - \mu}{\sigma}\)

  • 消除特征间的量纲差异
  • 注意:测试集必须用训练集的 \(\mu\)\(\sigma\),防止数据泄露

第九步:训练线性回归模型

# 创建并训练模型
model = LinearRegression()
model.fit(X_train, y_train)

# 在测试集上预测
y_pred = model.predict(X_test)

线性回归的核心假设

\[ \large y = w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + b \]

  • \(w_i\):在其他入模变量与线性假设给定时的条件关联;大小受量纲和编码影响,不等同于重要性或因果影响
  • \(b\):截距,所有特征为 0 时的基准销售额

第十步:模型评估——R² 分数

r2 = r2_score(y_test, y_pred)
print('R平方值(R²):', r2)

即时警示\(R^2\) 不是预测成本,也不保证优于朴素基线;脱离时间留出与误差比较,不能据此上线。

R² 决定系数 描述当前验证样本中相对均值基线的平方误差改善:

\[ \large R^2 = 1 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2} \]

R² 值范围 含义
高于 0 在该样本上优于“恒预测均值”;仍需比较时间基线与成本
等于 0 与均值预测相当
小于 0 比均值预测更差;应拒绝或重新计算

特征工程详解

展开完整代码(投影默认折叠)
# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行

# =============================================================================
# 题目:特征工程与特征相关性分析
# =============================================================================
# 本代码进行特征工程,包括相关性分析和特征选择。通过可视化各特征
# 与目标变量的关系,识别重要特征并剔除冗余特征,提升模型性能和
# 可解释性。这是机器学习项目中至关重要的一步。

# ==================== 定义特征列表 ====================
# 选择可能影响销售额的特征变量
features = ['Holiday_Flag', 'Temperature', 'Fuel_Price', 'CPI', 'Unemployment', 'month']
# Holiday_Flag: 假期标志,假期销售通常更高
# Temperature: 温度,可能影响购物意愿
# Fuel_Price: 燃油价格,影响消费者的出行和购买力
# CPI: 消费价格指数,衡量通胀水平
# Unemployment: 失业率,反映经济状况
# month: 月份,捕捉季节性规律

# 定义目标变量(要预测的变量)
target = 'Weekly_Sales'  # 周销售额是本项目的预测目标

# ==================== 绘制散点图矩阵 ====================
# 使用Seaborn绘制散点图矩阵,展示各特征与目标变量的关系
sns.pairplot(
    df,  # 数据源DataFrame
    x_vars=features,  # x轴变量列表(所有特征)
    y_vars=[target],  # y轴变量列表(目标变量)
    height=4,  # 每个子图的高度(英寸)
    aspect=0.7,  # 子图的宽高比(宽度/高度)
    kind='reg'  # 绘制带回归线的散点图
)
plt.suptitle('各特征与销售额的关系', y=1.02)  # 设置总标题,y=1.02将标题向上移动避免重叠
plt.show()  # 显示散点图矩阵

# ==================== 计算相关性矩阵 ====================
# 创建数据副本,避免修改原始数据
df1 = df.copy(deep=True)  # deep=True创建深拷贝,包括数据和索引

# 删除Store列(分类变量,无法计算相关系数)
df1.drop(['Store'], axis=1, inplace=True)  # axis=1表示删除列,inplace=True直接在原数据上修改

# 计算所有数值变量的相关系数矩阵
corr_matrix = df1.corr()  # corr()方法计算皮尔逊相关系数,返回对称矩阵
# 相关系数范围[-1, 1],绝对值越大相关性越强

# ==================== 绘制相关性热力图 ====================
plt.figure(figsize=(10, 8))  # 创建画布,尺寸为10x8英寸

# 使用Seaborn绘制热力图,可视化相关系数矩阵
sns.heatmap(corr_matrix, annot=True, cmap='Reds', center=0)
# annot=True在每个格子中显示相关系数的数值
# cmap='Reds'使用红色系配色方案(颜色越深相关性越强)
# center=0将颜色映射的中心设为0(白色表示无相关)

plt.title('特征相关性热力图')  # 设置图表标题
plt.show()  # 显示热力图

# ==================== 特征候选诊断 ====================
# 低边际相关不能证明Fuel_Price没有增量预测价值,因此保留至训练期内消融验证
# 是否删除须在同一时间留出窗比较季节朴素基线、成本误差与残差稳定性

# ==================== 输出解读 ====================
# 散点图矩阵展示了各特征与销售额的线性关系:
# - 如果点的分布近似直线,说明存在线性关系
# - 回归线斜率受量纲与混杂影响,只描述边际关联而非显著影响
# - 点很分散只说明当前线性边际关系弱,仍须检查交互、非线性和增量价值
#
# 热力图展示了所有特征之间的相关性:
# - 红色越深表示正相关越强
# - 蓝色越深表示负相关越强
# - 白色表示无相关
#
# 特征选择原则:
# 1. 在训练期内做特征消融,不用边际相关一票删除
# 2. 对共线性做条件诊断,并在同一时间段验证中比较泛化误差
# 3. 只有增量表现、成本与稳定性共同满足要求才保留特征

线性回归模型构建详解

依据诊断:折叠代码保留平台教学路径,但其中随机切分、固定 R² 档位、按原始系数绝对值排序及“影响销售”的措辞均不是部署依据。参考流程是时间留出 → 训练折内预处理 → 季节/朴素基线 → 消融或滚动验证 → 成本误差 → 残差与漂移检查;系数只解释为条件关联。

展开完整代码(投影默认折叠)
# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行

# =============================================================================
# 题目:构建线性回归模型预测销售额
# =============================================================================
# 本代码完成机器学习建模的完整流程:数据预处理、特征编码、数据标准化、
# 模型训练和评估。线性回归是最基础的预测模型,假设特征与目标变量之间
# 存在线性关系,适合作为基准模型。

# ==================== 分类变量编码 ====================
# 将分类变量转换为虚拟变量(独热编码)
df = pd.get_dummies(df, columns=['Store', 'month'])
# pd.get_dummies将分类变量转换为0/1指示变量
# columns参数指定要编码的列
# 例如:Store列有45个不同商店,会生成45个新列(Store_1, Store_2, ..., Store_45)
# 每行只有一个Store列为1,其余为0

# ==================== 准备特征和标签 ====================
# 提取特征矩阵X(删除目标变量列)
X = df.drop([target], axis=1)  # axis=1表示删除列
# X是一个DataFrame,包含所有特征列

# 提取目标变量y(销售额)
y = df[target]  # y是一个Series,包含所有销售额值

# ==================== 划分训练集和测试集 ====================
# 将数据划分为训练集(80%)和测试集(20%)
X_train, X_test, y_train, y_test = train_test_split(
    X,  # 特征矩阵
    y,  # 目标变量
    train_size=0.8,  # 训练集占比80%
    test_size=0.2,  # 测试集占比20%
    random_state=100  # 随机种子,保证每次运行结果相同
)
# train_test_split随机打乱数据并按指定比例划分
# 返回四个数组:训练集特征、测试集特征、训练集标签、测试集标签

# ==================== 数据标准化 ====================
# 创建标准化器(将数据转换为均值为0、标准差为1的分布)
scaler = StandardScaler()  # StandardScaler使用z-score标准化: (x - mean) / std

# 在训练集上拟合标准化器,并转换训练集数据
X_train_scaled = scaler.fit_transform(X_train)  # fit计算均值和标准差,transform应用标准化

# 使用训练集的均值和标准差转换测试集数据
X_test_scaled = scaler.transform(X_test)  # 注意:测试集使用训练集的统计量,不重新计算
# 这样确保模型在测试时看到的数据分布与训练时一致

# ==================== 训练线性回归模型 ====================
# 创建线性回归模型对象
model = LinearRegression()  # LinearRegression实现了普通最小二乘法(OLS)

# 在训练数据上拟合模型
model.fit(X_train_scaled, y_train)  # fit方法学习特征与目标变量的线性关系
# 模型学习: y = w1*x1 + w2*x2 + ... + wn*xn + b
# 其中w是权重(系数),b是截距

# ==================== 模型预测 ====================
# 使用训练好的模型预测测试集的销售额
y_pred = model.predict(X_test_scaled)  # predict方法返回预测的销售额

# ==================== 模型评估 ====================
# 计算R²分数(决定系数),评估模型拟合优度
r2 = r2_score(y_test, y_pred)  # R²可为负;只描述该测试样本相对均值基线的平方误差改善
# R² = 1 - (残差平方和 / 总平方和)
# 表示模型解释的方差占总方差的比例

print(f"R²值: {r2:.4f}")  # 打印R²值,保留4位小数

# 将R²标为相对均值基线的样本内度量,避免解释为因果贡献
print(f"相对均值基线的R²为{r2:.4f}")  # 仍须与时间基线和成本误差比较

# ==================== 输出解读 ====================
# 线性回归模型的评估指标解读:
# 1. R²值(决定系数):
#    - R² = 1: 完美拟合(所有点都在回归线上)
#    - 不使用通用档位;须在同一时间留出窗与季节朴素基线和成本误差比较
#    - R² = 0: 模型与简单猜测(用均值预测)一样差
#
# 2. 模型系数(model.coef_):
#    - 系数表示其他入模变量给定时的条件关联,不自动具有因果含义
#    - 正负号描述模型中的条件方向,须结合编码、尺度和不确定性解释
#    - 不同量纲下的系数绝对值不能直接比较重要性
#
# 3. 截距(model.intercept_):
#    - 当所有特征为0时的销售额(基准值)
#
# 4. 残差分析:
#    - 残差 = 真实值 - 预测值
#    - 理想情况下,残差应服从正态分布,均值为0
#    - 如果残差有模式,说明模型有改进空间
#
# 5. 实际应用:
#    - 超市可以根据预测结果提前备货
#    - 识别需要进一步消融、稳健性和业务验证的候选因素
#    - 优化营销策略和人员安排

本章总结

本章知识点总结 零售超市销售额预测项目的核心知识点汇总 核心知识点回顾 数据预处理 日期特征提取 缺失值处理 数据类型转换 特征工程 相关性分析 特征选择与剔除 独热编码 模型构建 训练/测试集划分 数据标准化 线性回归训练 模型评估 R² 决定系数 残差分析 业务解读 关键要点 时间留出防泄漏 · 与朴素基线同一时间段比较 · R² 不替代成本误差

随堂练习

  • 问题 1|数据口径与版本如何确定?:将累计营业收入差分为单季值;按公告日保留当时可得版本,列出期间、字段和重复版本处理。
  • 问题 2|怎样比较两种预测方法?:预先固定训练/验证分界,不得随机打乱;在同一验证窗口比较上年同季朴素基线与趋势回归 MAPE。
  • 问题 3|怎样说明预测的不确定性?:为每个验证季报告预测和近似 95% 区间;说明残差区间的局限,并确认验证期未进入拟合或预处理。
  • 问题 4|在不同成本要求下怎样选择?:在 10%/15%/20% 三档成本阈值下判断采用回归、保留基线或暂缓应用,写明负责人、需要重新检查的情况与下季度再次检查时间。
  • 作答提示:代码、单季真实值、基准预测、模型预测、区间表和两个 MAPE 缺一不可。若文件读取失败,请从课程数据下载入口重新下载并核对路径,不要用自行编造的数值代替。

教师参考解答

教师参考解答|答案与说明 1

  • 所用数据与字段:可用资产 stock/financial_statement.h5(key: financial_data);百联股份 600827.SH;标准化字段 ts_code/end_date/total_revenue,可选版本字段 ann_date/f_ann_date
  • 完整答案路线:本章前置块 lst-ch22-local-bailian-baseline 完成版本去重、累计收入转单季值、固定时间切分和上年同季基线;lst-ch22-local-bailian-evaluation 在同一验证窗生成趋势回归、两个 MAPE 与近似 95% 区间。以下判断条件代码完成三档决策,三部分共同逐项覆盖 20/30/25/25 分检查。

教师参考解答|代码 1

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具以定位规定财报快照
import numpy as np  # 导入数值工具以做有限值检查要求
import pandas as pd  # 导入表格工具以处理季度财报
from sklearn.linear_model import LinearRegression  # 导入趋势回归候选模型
from sklearn.metrics import r2_score  # 导入R方以描述验证期拟合边界
financial_path=Path('/home/ubuntu/r2_data_mount/data/stock/financial_statement.h5')  # 绑定百联股份财报快照
if not financial_path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置')  # 缺少规定资产时终止
financials=pd.read_hdf(financial_path,key='financial_data',where="order_book_id == '600827.XSHG'",columns=['order_book_id','quarter','info_date','operating_revenue']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','info_date':'ann_date','operating_revenue':'total_revenue'})  # 按公司读取必需披露字段
financials['ts_code']=financials['ts_code'].str.replace('.XSHG','.SH',regex=False)  # 将证券代码后缀统一为课程口径
financials['end_date']=pd.PeriodIndex(financials['end_date'].str.upper(),freq='Q').end_time.normalize()  # 将季度键转为报告期末日
financials['ann_date']=pd.to_datetime(financials['ann_date'],errors='coerce')  # 解析披露日侜为版本排序键
if financials[['ts_code','end_date','ann_date']].isna().any().any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 版本键缺失时请先检查数据后再继续
bailian=financials.sort_values(['ts_code','end_date','ann_date']).drop_duplicates(['ts_code','end_date'],keep='last').copy()  # 仅保留每个报告期最晚披露版本
bailian=bailian.loc[bailian['end_date'].between('2015-01-01','2025-12-31')].sort_values('end_date').copy()  # 固定十一年样本边界
bailian['fiscal_year']=bailian['end_date'].dt.year  # 构造财政年键
bailian['quarter']=bailian['end_date'].dt.quarter  # 构造财政季度键
prior_cumulative=bailian[['fiscal_year','quarter','total_revenue']].rename(columns={'total_revenue':'prior_cumulative_revenue'}).copy()  # 建立前一季累计收入查找表
prior_cumulative['quarter']=prior_cumulative['quarter']+1  # 把查找键前移到后一季
bailian=bailian.merge(prior_cumulative,on=['fiscal_year','quarter'],how='left',validate='one_to_one')  # 按同年紧邻季度连接累计值
if (bailian['quarter'].gt(1)&bailian['prior_cumulative_revenue'].isna()).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺前季时禁止差分
bailian['quarter_revenue']=np.where(bailian['quarter'].eq(1),bailian['total_revenue'],bailian['total_revenue']-bailian['prior_cumulative_revenue'])  # 把累计值转为单季收入

教师参考解答|代码 2

展开代码(代码区可独立滚动)
prior_season=bailian[['fiscal_year','quarter','quarter_revenue']].rename(columns={'quarter_revenue':'seasonal_baseline'}).copy()  # 建立上年同季基线查找表
prior_season['fiscal_year']=prior_season['fiscal_year']+1  # 将历史财政年键前移一年
bailian=bailian.merge(prior_season,on=['fiscal_year','quarter'],how='left',validate='one_to_one')  # 按同季连接上年基线
bailian['trend_index']=range(len(bailian))  # 构造仅依赖时间顺序的趋势特征
model_frame=bailian.dropna(subset=['quarter_revenue','seasonal_baseline']).copy()  # 仅保留可同一时间段比较的季度
model_features=pd.get_dummies(model_frame[['trend_index','quarter']],columns=['quarter'],drop_first=True)  # 构造趋势与季节虚拟特征
training_mask=model_frame['end_date'].lt('2023-01-01')  # 固定2023年前为训练窗
validation_mask=model_frame['end_date'].between('2023-01-01','2024-12-31')  # 固定2023—2024为独立验证窗
if training_mask.sum()<8 or validation_mask.sum()<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 训练或验证季度不足时终止
if model_frame.loc[validation_mask,'quarter_revenue'].eq(0).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 验证实际值为零时禁用MAPE
regression=LinearRegression().fit(model_features.loc[training_mask],model_frame.loc[training_mask,'quarter_revenue'])  # 仅用训练窗拟合候选模型
model_frame.loc[validation_mask,'regression_forecast']=regression.predict(model_features.loc[validation_mask])  # 生成验证窗趋势预测
training_residuals=model_frame.loc[training_mask,'quarter_revenue']-regression.predict(model_features.loc[training_mask])  # 仅从训练残差估计区间宽度
interval_half_width=1.96*training_residuals.std(ddof=1)  # 在近似正态同方差且忽略参数不确定性时计算课堂95%近似半宽
validation=model_frame.loc[validation_mask].copy()  # 隔离用于模型决策的验证表
validation['prediction_lower']=validation['regression_forecast']-interval_half_width  # 计算近似区间下界
validation['prediction_upper']=validation['regression_forecast']+interval_half_width  # 计算近似区间上界
baseline_mape=((validation['quarter_revenue']-validation['seasonal_baseline']).abs()/validation['quarter_revenue'].abs()).mean()  # 计算上年同季基线MAPE
regression_mape=((validation['quarter_revenue']-validation['regression_forecast']).abs()/validation['quarter_revenue'].abs()).mean()  # 计算趋势模型MAPE
validation_r2=r2_score(validation['quarter_revenue'],validation['regression_forecast'])  # 计算验证窗R方作为描述边界
if not np.isfinite([baseline_mape,regression_mape,validation_r2,interval_half_width]).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 非有限依据不得进入决策

教师参考解答|代码 3

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具检查规定资产
financial_path=Path('/home/ubuntu/r2_data_mount/data/stock/financial_statement.h5')  # 绑定百联股份财报快照
if not financial_path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置')  # 缺失时如实停止
cost_thresholds=[0.10,0.15,0.20]  # 定义课堂成本敏感性情景
decision_owner='零售预测负责人(课堂角色,待业务确认)'  # 指定课堂责任角色
recheck_date='下一季度财报公布后第5个工作日'  # 事先设定再次检查日期规则
decision_table=[]  # 初始化逐情景决策表
for cost_threshold in cost_thresholds:  # 逐档比较同一时间段模型误差
    if regression_mape<baseline_mape and regression_mape<=cost_threshold:  # 判断候选模型是否同时优于基线并过最低要求
        decision_status='ADOPT_REGRESSION'  # 采用回归并进入监控
    elif baseline_mape<=cost_threshold:  # 判断朴素基线是否满足预算
        decision_status='KEEP_BASELINE'  # 保留较稳健的季节基线
    else:  # 两种方法都不能满足成本最低要求
        decision_status='暂不采用'  # 暂停上线并调查数据与结构变化
    action={'ADOPT_REGRESSION':'小范围试用并继续观察','KEEP_BASELINE':'保留基准方法并核对','暂不采用':'暂不采用并检查原因'}[decision_status]  # 记录建议
    recheck_when='实际MAPE连续两期超过该课堂阈值或输入字段改变'  # 明确升级与需要重新检查的情况
    decision_table.append({'threshold':cost_threshold,'threshold_status':'课堂情景,待业务规定/预算批准','decision':decision_status,'owner':decision_owner,'action':action,'recheck_when':recheck_when,'recheck_date':recheck_date})  # 保存完整决策依据
print(validation,decision_table)  # 输出验证表与三档决策

教师参考解答|答案与说明 2

  • 参考结果:单季真实值、同季基准预测、回归预测、区间上下界、两个 MAPE,以及不同误差要求下的选择和下季度复查时间。课程数据可从下载入口取得;若读取失败,请核对下载文件、路径和字段后重新运行。
  • 边界 / 局限:累计值先转流量;验证窗不得参与调参
  • 常见错误:切换到未经定义的 case 资产;随机切分;模型比较使用不同一时间段口